﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CRM_SharePoint_Transports
{
    public class CRMReceiveTransport : BlueIntegrator.Extensibility.ReceiveTransportBase
    {
        public override void Initialise(string transportName, string portId, System.Collections.IDictionary transportParameters)
        {
            m_portId = portId;
            m_dal = transportParameters["__DAL"] as BlueIntegrator.DAL.DALBase;
            m_properties = new System.Collections.Hashtable();

            foreach (System.Collections.DictionaryEntry dent in transportParameters)
            {
                m_properties[dent.Key] = BlueIntegrator.Messaging.SettingTokenHelper.ResolveTokens(m_dal, BlueIntegrator.Helpers.Conversion.SafeGetString(dent.Value));
            }
        }

        bool m_bLocked = false;
        
        public override bool GetMessage(out System.IO.Stream messageContents, out System.Collections.IDictionary contextParameters, out object callBackContext)
        {
            contextParameters = null;
            messageContents = null;
            callBackContext = null;

            if (!m_bLocked)
                m_bLocked = m_dal.LockObject("RTL" + m_portId);

            if (!m_bLocked)
                return false;

            System.Xml.XmlDocument result = SSISLib.CRMAPI.GetData(m_properties, null, m_dal);
            m_pendingResult = null;

            // maybe refine?
            if (BlueIntegrator.Helpers.Conversion.ConvertToBool(m_properties["ReturnDelta"]))
            {
                System.IO.Stream blobStr = m_dal.LoadBlobData(m_portId);

                m_pendingResult = new System.IO.MemoryStream();
                result.Save(m_pendingResult);
                m_pendingResult.Position = 0;

                if (blobStr != null)
                {
                    System.Xml.XmlDocument doc = new System.Xml.XmlDocument();
                    doc.Load(blobStr);
                    ProcessDifferences(result, doc);
                }
                else
                {
                    ProcessDifferences(result, null);
                }
            }

            messageContents = new System.IO.MemoryStream();
            result.Save(messageContents);

            if (m_pendingResult == null)
                m_pendingResult = new System.IO.MemoryStream((messageContents as System.IO.MemoryStream).ToArray());

            contextParameters = new System.Collections.Hashtable();
            return result.DocumentElement != null && result.DocumentElement.ChildNodes.Count > 0;
        }

        public override void CommitReceivedMessage(object callBackContext, System.Collections.IDictionary contextParameters)
        {
            if (BlueIntegrator.Helpers.Conversion.ConvertToBool(m_properties["ReturnDelta"]))
            {
                m_dal.SaveBlobData(m_portId, m_pendingResult);
            }

            m_pendingResult = null;

            if (m_bLocked)
            {
                m_bLocked = false;
                m_dal.UnlockObject("RTL" + m_portId);
            }
        }

        public override bool CancelReceivedMessage(object callBackContext)
        {
            m_pendingResult = null;

            if (m_bLocked)
            {
                m_bLocked = false;
                m_dal.UnlockObject("RTL" + m_portId);
            }
            
            return true;
        }

        public override System.Windows.Forms.Control[] GetTabPages()
        {
            return new System.Windows.Forms.Control[] { new CRMTransportConfigControl(false) };
        }

        BlueIntegrator.DAL.DALBase m_dal;
        string m_portId;
        System.Collections.Hashtable m_properties;
        System.IO.MemoryStream m_pendingResult = null;

                /// <summary>
        /// Difference processing
        /// </summary>
        /// <param name="newDoc"></param>
        /// <param name="oldDoc"></param>
        private void ProcessDifferences(System.Xml.XmlDocument newDoc, System.Xml.XmlDocument oldDoc)
        {
            ProcessNode(newDoc, oldDoc);
        }

        /// <summary>
        /// Difference processing recursive
        /// </summary>
        /// <param name="newNode"></param>
        /// <param name="correspondingNode"></param>
        private void ProcessNode(System.Xml.XmlNode newNode, System.Xml.XmlNode correspondingNode)
        {
            System.Collections.Hashtable seenIDs = new System.Collections.Hashtable();
            System.Collections.Generic.List<System.Xml.XmlNode> toDelete = new List<System.Xml.XmlNode>();

            foreach (System.Xml.XmlNode child in newNode.ChildNodes)
            {
                if (!(child is System.Xml.XmlElement))
                    continue;

                string id = GetNodeID(child);

                if (id == null)
                    continue;

                seenIDs[id] = "1";

                if (correspondingNode == null)
                {
                    if (id != "ROOT")
                        SetNodeStatus(child, "Insert");
                    ProcessNode(child, null);
                }
                else
                {
                    bool found = false;

                    foreach (System.Xml.XmlNode child2 in correspondingNode.ChildNodes)
                    {
                        if (GetNodeID(child2) == id)
                        {
                            found = true;
                            bool bThisChanged = false, bChildChanged = false;
                            GetNodeChangeStatus(child, child2, out bThisChanged, out bChildChanged);

                            if (!bThisChanged && !bChildChanged)
                            {
                                toDelete.Add(child);
                            }
                            else if (!bThisChanged)
                            {
                                SetNodeStatus(child, "");
                            }
                            else
                            {
                                SetNodeStatus(child, "Update");
                            }

                            if (bChildChanged)
                                ProcessNode(child, child2);
                        }
                    }

                    if (!found)
                    {
                        SetNodeStatus(child, "Insert");
                        ProcessNode(child, null);
                    }
                }
            }


            foreach (System.Xml.XmlNode node in toDelete)
            {
                newNode.RemoveChild(node);
            } 
            
            if (correspondingNode != null)
            {
                foreach (System.Xml.XmlNode child2 in correspondingNode.ChildNodes)
                {
                    if (!(child2 is System.Xml.XmlElement))
                        continue;

                    string id = GetNodeID(child2);

                    if (id == null)
                        continue;

                    if (seenIDs[id] == null)
                    {
                        // flag record as deleted
                        System.Xml.XmlNode deletedNode = newNode.OwnerDocument.ImportNode(child2, true);
                        SetNodeStatus(deletedNode, "Delete");
                        newNode.AppendChild(deletedNode);
                    }
                }
            }
        }

        /// <summary>
        /// Looks up entity id
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private string GetNodeID(System.Xml.XmlNode node)
        {
            string soughtAttribute = "ID";

            foreach (System.Xml.XmlAttribute attr in node.Attributes)
            {
                if (string.Compare(attr.Name, soughtAttribute, true) == 0)
                {
                    return attr.Value;
                }
            }

            if (node == node.OwnerDocument.DocumentElement)
            {
                return "ROOT";
            }

            return null;
        }

        /// <summary>
        /// Updates the status of an item
        /// </summary>
        /// <param name="statusNode"></param>
        /// <param name="status"></param>
        private void SetNodeStatus(System.Xml.XmlNode statusNode, string status)
        {
            bool bFound = false;

            foreach (System.Xml.XmlNode node in statusNode.ChildNodes)
            {
                if (node is System.Xml.XmlElement && node.Name == "Action")
                {
                    bFound = true;
                    if (node.Attributes["ActionType"] != null)
                    {
                        node.Attributes["ActionType"].Value = status;
                    }
                    else
                    {
                        System.Xml.XmlAttribute attr = statusNode.OwnerDocument.CreateAttribute("ActionType");
                        attr.Value = status;
                        node.Attributes.Append(attr);
                    }

                    break;
                }
            }

            if (!bFound && status != "")
            {
                System.Xml.XmlElement el = statusNode.OwnerDocument.CreateElement("Action");
                System.Xml.XmlAttribute attr = statusNode.OwnerDocument.CreateAttribute("ActionType");
                attr.Value = status;
                el.Attributes.Append(attr);
                statusNode.PrependChild(el);
            }
        }

        /// <summary>
        /// Works out what has changed between two nodes...
        /// </summary>
        /// <param name="n1"></param>
        /// <param name="n2"></param>
        /// <param name="bThisChange"></param>
        /// <param name="bChildChange"></param>
        private void GetNodeChangeStatus(System.Xml.XmlNode n1, System.Xml.XmlNode n2, out bool bThisChange, out bool bChildChange)
        {
            // first high level
            bThisChange = false;

            if (n1.Attributes.Count != n2.Attributes.Count)
            {
                bThisChange = true;
            }

            if (!bThisChange)
            {
                int nAttr = n1.Attributes.Count;

                for (int i=0; i < nAttr && !bThisChange; i++)
                {
                    string val1 = n1.Attributes[i].Value;

                    try
                    {
                        string val2 = n2.Attributes[n1.Attributes[i].Name].Value;

                        bThisChange = val1 != val2;
                    }
                    catch
                    {
                        bThisChange = true;
                    }
                }
            }

            // then lower
            bChildChange = n1.InnerXml != n2.InnerXml;
        }
    }
}
